// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:collection/collection.dart' hide mapMap;
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';

import 'exceptions.dart';
import 'io.dart';
import 'language_version.dart';
import 'package.dart';
import 'package_name.dart';
import 'pubspec_parse.dart';
import 'sdk.dart';
import 'source.dart';
import 'source/root.dart';
import 'system_cache.dart';

export 'pubspec_parse.dart' hide PubspecBase;

/// The default SDK upper bound constraint for packages that don't declare one.
///
/// This provides a sane default for packages that don't have an upper bound.
final VersionRange _defaultUpperBoundSdkConstraint =
    VersionConstraint.parse('<2.0.0') as VersionRange;

/// The parsed contents of a pubspec file.
///
/// The fields of a pubspec are, for the most part, validated when they're first
/// accessed. This allows a partially-invalid pubspec to be used if only the
/// valid portions are relevant. To get a list of all errors in the pubspec, use
/// [allErrors].
class Pubspec extends PubspecBase {
  // If a new lazily-initialized field is added to this class and the
  // initialization can throw a [PubspecException], that error should also be
  // exposed through [allErrors].

  /// The fields of [pubspecOverridesFilename]. `null` if no such file exists or
  /// has to be considered.
  final YamlMap? _overridesFileFields;

  String? get _packageName => fields['name'] != null ? name : null;

  final bool dependencyOverridesFromOverridesFile;

  /// The name of the manifest file.
  static const pubspecYamlFilename = 'pubspec.yaml';

  /// The filename of the pubspec overrides file.
  ///
  /// This file can contain dependency_overrides that override those in
  /// pubspec.yaml.
  static const pubspecOverridesFilename = 'pubspec_overrides.yaml';

  /// The registry of sources to use when parsing [dependencies] and
  /// [devDependencies].
  final SourceRegistry sources;

  /// It is used to resolve relative paths. And to resolve path-descriptions
  /// from a git dependency as git-descriptions.
  final ResolvedDescription _containingDescription;

  /// Directories of packages that should resolve together with this package.
  late List<String> workspace = () {
    final result = <String>[];
    final workspaceNode =
        _overridesFileFields?.nodes['workspace'] ?? fields.nodes['workspace'];
    if (workspaceNode != null && !languageVersion.supportsWorkspaces) {
      _error(
        '`workspace` and `resolution` requires at least language version '
        '${LanguageVersion.firstVersionWithWorkspaces}',
        workspaceNode.span,
        hint: '''
Consider updating the SDK constraint to:

environment:
  sdk: '^${sdk.version}'
''',
      );
    }
    if (workspaceNode == null || workspaceNode.value == null) return <String>[];

    if (workspaceNode is! YamlList) {
      _error('"workspace" must be a list of strings', workspaceNode.span);
    }
    for (final t in workspaceNode.nodes) {
      final value = t.value;
      if (value is! String) {
        _error('"workspace" must be a list of strings', t.span);
      }
      if (!p.isRelative(value)) {
        _error('"workspace" members must be relative paths', t.span);
      }
      if (p.equals(value, '.') || !p.isWithin('.', value)) {
        _error('"workspace" members must be subdirectories', t.span);
      }
      result.add(value);
    }
    return result;
  }();

  /// The resolution mode.
  late Resolution resolution = () {
    final resolutionNode =
        _overridesFileFields?.nodes['resolution'] ?? fields.nodes['resolution'];

    if (resolutionNode != null && !languageVersion.supportsWorkspaces) {
      _error(
        '`workspace` and `resolution` requires at least language version '
        '${LanguageVersion.firstVersionWithWorkspaces}',
        resolutionNode.span,
        hint: '''
Consider updating the SDK constraint to:

environment:
  sdk: '^${sdk.version}'
''',
      );
    }
    return switch (resolutionNode?.value) {
      null => Resolution.none,
      'local' => Resolution.local,
      'workspace' => Resolution.workspace,
      'external' => Resolution.external,
      _ => _error(
        '"resolution" must be one of `workspace`, `local`, `external`',
        resolutionNode!.span,
      ),
    };
  }();

  /// The additional packages this package depends on.
  Map<String, PackageRange> get dependencies =>
      _dependencies ??= _parseDependencies(
        'dependencies',
        fields.nodes['dependencies'],
        sources,
        languageVersion,
        _packageName,
        _containingDescription,
      );

  Map<String, PackageRange>? _dependencies;

  /// The packages this package depends on when it is the root package.
  Map<String, PackageRange> get devDependencies =>
      _devDependencies ??= _parseDependencies(
        'dev_dependencies',
        fields.nodes['dev_dependencies'],
        sources,
        languageVersion,
        _packageName,
        _containingDescription,
      );

  Map<String, PackageRange>? _devDependencies;

  /// The dependency constraints that this package overrides when it is the root
  /// package.
  ///
  /// Dependencies here will replace any dependency on a package with the same
  /// name anywhere in the dependency graph.
  ///
  /// These can occur both in the pubspec.yaml file and the
  /// [pubspecOverridesFilename].
  Map<String, PackageRange> get dependencyOverrides {
    if (_dependencyOverrides != null) return _dependencyOverrides!;
    final pubspecOverridesFields = _overridesFileFields;
    if (pubspecOverridesFields != null) {
      if (pubspecOverridesFields.containsKey('dependency_overrides')) {
        _dependencyOverrides = _parseDependencies(
          'dependency_overrides',
          pubspecOverridesFields.nodes['dependency_overrides'],
          sources,
          languageVersion,
          _packageName,
          _containingDescription,
          fileType: _FileType.pubspecOverrides,
        );
      }
    }
    return _dependencyOverrides ??= _parseDependencies(
      'dependency_overrides',
      fields.nodes['dependency_overrides'],
      sources,
      languageVersion,
      _packageName,
      _containingDescription,
    );
  }

  Map<String, PackageRange>? _dependencyOverrides;

  SdkConstraint get dartSdkConstraint => sdkConstraints['dart']!;

  /// A map from SDK identifiers to constraints on those SDK versions.
  late final Map<String, SdkConstraint> sdkConstraints =
      _givenSdkConstraints ?? UnmodifiableMapView(_parseEnvironment(fields));

  final Map<String, SdkConstraint>? _givenSdkConstraints;

  /// Whether or not to apply the [_defaultUpperBoundSdkConstraint] to this
  /// pubspec.
  final bool _includeDefaultSdkConstraint;

  /// Parses the "environment" field in [parent] and returns a map from SDK
  /// identifiers to constraints on those SDKs.
  Map<String, SdkConstraint> _parseEnvironment(YamlMap parent) {
    final yaml = parent['environment'];
    final VersionConstraint originalDartSdkConstraint;
    if (yaml == null) {
      originalDartSdkConstraint = VersionConstraint.any;
    } else if (yaml is! YamlMap) {
      _error(
        '"environment" field must be a map.',
        parent.nodes['environment']!.span,
      );
    } else {
      originalDartSdkConstraint = _parseVersionConstraint(
        yaml.nodes['sdk'],
        _packageName,
        _FileType.pubspec,
      );
    }
    final dartConstraint = SdkConstraint.interpretDartSdkConstraint(
      originalDartSdkConstraint,
      defaultUpperBoundConstraint:
          _includeDefaultSdkConstraint ? _defaultUpperBoundSdkConstraint : null,
    );
    final constraints = {'dart': dartConstraint};

    if (yaml is YamlMap) {
      yaml.nodes.forEach((nameNode, constraintNode) {
        if (nameNode is! YamlNode) throw AssertionError('Bad state');
        final name = nameNode.value;
        if (name is! String) {
          _error('SDK names must be strings.', nameNode.span);
        } else if (name == 'dart') {
          _error('Use "sdk" to for Dart SDK constraints.', nameNode.span);
        }
        if (name == 'sdk') return;

        final constraint = _parseVersionConstraint(
          constraintNode,
          _packageName,
          _FileType.pubspec,
        );
        constraints[name] =
            name == 'flutter'
                ? SdkConstraint.interpretFlutterSdkConstraint(
                  constraint,
                  isRoot: _containingDescription is ResolvedRootDescription,
                  languageVersion: dartConstraint.languageVersion,
                )
                : SdkConstraint(constraint);
      });
    }
    return constraints;
  }

  /// The language version implied by the sdk constraint.
  LanguageVersion get languageVersion {
    return LanguageVersion.fromSdkConstraint(
      dartSdkConstraint.originalConstraint,
    );
  }

  /// Loads the pubspec for a package located in [packageDir].
  ///
  /// If [expectedName] is passed and the pubspec doesn't have a matching name
  /// field, this will throw a [SourceSpanApplicationException].
  ///
  /// If [allowOverridesFile] is `true` [pubspecOverridesFilename] is loaded and
  /// is allowed to override dependency_overrides from `pubspec.yaml`.
  factory Pubspec.load(
    String packageDir,
    SourceRegistry sources, {
    String? expectedName,
    bool allowOverridesFile = false,
    required ResolvedDescription containingDescription,
  }) {
    final pubspecPath = p.join(packageDir, pubspecYamlFilename);
    final overridesPath = p.join(packageDir, pubspecOverridesFilename);
    if (!fileExists(pubspecPath)) {
      throw FileException(
        // Make the package dir absolute because for the entrypoint it'll just
        // be ".", which may be confusing.
        'Could not find a file named "pubspec.yaml" in '
        '"${canonicalize(packageDir)}".',
        pubspecPath,
      );
    }
    final overridesFileContents =
        allowOverridesFile && fileExists(overridesPath)
            ? readTextFile(overridesPath)
            : null;

    return Pubspec.parse(
      readTextFile(pubspecPath),
      sources,
      expectedName: expectedName,
      location: p.toUri(pubspecPath),
      overridesFileContents: overridesFileContents,
      overridesLocation: p.toUri(overridesPath),
      containingDescription: containingDescription,
    );
  }

  /// Convenience helper to pass to [Package.load].
  static Pubspec Function(
    String dir, {
    String? expectedName,
    required bool withPubspecOverrides,
  })
  loadRootWithSources(SourceRegistry sources) {
    return (
      String dir, {
      String? expectedName,
      required bool withPubspecOverrides,
    }) => Pubspec.load(
      dir,
      sources,
      expectedName: expectedName,
      allowOverridesFile: withPubspecOverrides,
      containingDescription: ResolvedRootDescription.fromDir(dir),
    );
  }

  Pubspec(
    String name, {
    Version? version,
    Iterable<PackageRange>? dependencies,
    Iterable<PackageRange>? devDependencies,
    Iterable<PackageRange>? dependencyOverrides,
    Map? fields,
    SourceRegistry? sources,
    Map<String, SdkConstraint>? sdkConstraints,
    this.workspace = const <String>[],
    this.dependencyOverridesFromOverridesFile = false,
    this.resolution = Resolution.none,
  }) : _dependencies =
           dependencies == null
               ? null
               : {for (final d in dependencies) d.name: d},
       _devDependencies =
           devDependencies == null
               ? null
               : {for (final d in devDependencies) d.name: d},
       _dependencyOverrides =
           dependencyOverrides == null
               ? null
               : {for (final d in dependencyOverrides) d.name: d},
       _givenSdkConstraints =
           sdkConstraints ??
           UnmodifiableMapView({'dart': SdkConstraint(VersionConstraint.any)}),
       _includeDefaultSdkConstraint = false,
       sources =
           sources ??
           ((String? name) => throw StateError('No source registry given')),
       _overridesFileFields = null,
       // This is a dummy value. Dependencies should already be resolved, so we
       // never need to do relative resolutions.
       _containingDescription = ResolvedRootDescription.fromDir('.'),
       super(
         fields == null ? YamlMap() : YamlMap.wrap(fields),
         name: name,
         version: version,
       );

  /// Returns a Pubspec object for an already-parsed map representing its
  /// contents.
  ///
  /// If [expectedName] is passed and the pubspec doesn't have a matching name
  /// field, this will throw an [ApplicationException].
  ///
  /// [location] is the location from which this pubspec was loaded.
  Pubspec.fromMap(
    Map fields,
    this.sources, {
    YamlMap? overridesFields,
    String? expectedName,
    Uri? location,
    required ResolvedDescription containingDescription,
  }) : _overridesFileFields = overridesFields,
       _includeDefaultSdkConstraint = true,
       _givenSdkConstraints = null,
       dependencyOverridesFromOverridesFile =
           overridesFields != null &&
           overridesFields.containsKey('dependency_overrides'),
       _containingDescription = containingDescription,
       super(
         fields is YamlMap ? fields : YamlMap.wrap(fields, sourceUrl: location),
       ) {
    if (overridesFields != null) {
      overridesFields.nodes.forEach((key, _) {
        final keyNode = key as YamlNode;
        if (!const {
          'dependency_overrides',
          'resolution',
          'workspace',
        }.contains(keyNode.value)) {
          throw SourceSpanApplicationException(
            'pubspec_overrides.yaml only supports the '
            '`dependency_overrides`, `resolution` and `workspace` fields.',
            keyNode.span,
          );
        }
      });
    }
    // If [expectedName] is passed, ensure that the actual 'name' field exists
    // and matches the expectation.
    if (expectedName == null) return;
    if (name == expectedName) return;

    throw SourceSpanApplicationException(
      '"name" field doesn\'t match expected name '
      '"$expectedName".',
      this.fields.nodes['name']!.span,
    );
  }

  /// Parses the pubspec stored at [location] whose text is [contents].
  ///
  /// If the pubspec doesn't define a version for itself, it defaults to
  /// [Version.none].
  factory Pubspec.parse(
    String contents,
    SourceRegistry sources, {
    String? expectedName,
    Uri? location,
    String? overridesFileContents,
    Uri? overridesLocation,
    required ResolvedDescription containingDescription,
  }) {
    final YamlMap pubspecMap;
    YamlMap? overridesFileMap;
    try {
      pubspecMap = _ensureMap(loadYamlNode(contents, sourceUrl: location));
      if (overridesFileContents != null) {
        overridesFileMap = _ensureMap(
          loadYamlNode(overridesFileContents, sourceUrl: overridesLocation),
        );
      }
    } on YamlException catch (error) {
      throw SourceSpanApplicationException(error.message, error.span);
    }

    return Pubspec.fromMap(
      pubspecMap,
      sources,
      overridesFields: overridesFileMap,
      expectedName: expectedName,
      location: location,
      containingDescription: containingDescription,
    );
  }

  Pubspec copyWith({
    String? name,
    Version? version,
    Iterable<PackageRange>? dependencies,
    Iterable<PackageRange>? devDependencies,
    Iterable<PackageRange>? dependencyOverrides,
    Map? fields,
    Map<String, SdkConstraint>? sdkConstraints,
    List<String>? workspace,
    //this.dependencyOverridesFromOverridesFile = false,
    Resolution? resolution,
  }) {
    return Pubspec(
      name ?? this.name,
      version: version ?? this.version,
      dependencies: dependencies ?? this.dependencies.values,
      devDependencies: devDependencies ?? this.devDependencies.values,
      dependencyOverrides:
          dependencyOverrides ?? this.dependencyOverrides.values,
      sdkConstraints: sdkConstraints ?? this.sdkConstraints,
      workspace: workspace ?? this.workspace,
      resolution: resolution ?? this.resolution,
    );
  }

  /// Ensures that [node] is a mapping.
  ///
  /// If [node] is already a map it is returned.
  /// If [node] is yaml-null an empty map is returned.
  /// Otherwise an exception is thrown.
  static YamlMap _ensureMap(YamlNode node) {
    if (node is YamlScalar && node.value == null) {
      return YamlMap(sourceUrl: node.span.sourceUrl);
    } else if (node is YamlMap) {
      return node;
    } else {
      throw SourceSpanApplicationException(
        'The pubspec must be a YAML mapping.',
        node.span,
      );
    }
  }

  List<SourceSpanApplicationException> _collectErrorsFor(
    List<dynamic Function()> toCheck,
  ) {
    final errors = <SourceSpanApplicationException>[];
    void collectError(void Function() fn) {
      try {
        fn();
      } on SourceSpanApplicationException catch (e) {
        errors.add(e);
      }
    }

    for (final fn in toCheck) {
      collectError(fn);
    }
    return errors;
  }

  /// Returns a list of errors relevant to consuming this pubspec as a
  /// dependency
  ///
  /// This will return at most one error for each field.
  List<SourceSpanApplicationException> get dependencyErrors =>
      _collectErrorsFor([
        () => name,
        () => version,
        () => dependencies,
        () => executables,
        () => ignoredAdvisories,
      ]);

  /// Returns a list of most errors in this pubspec.
  ///
  /// This will return at most one error for each field.
  List<SourceSpanApplicationException> get allErrors => _collectErrorsFor([
    () => name,
    () => version,
    () => dependencies,
    () => devDependencies,
    () => publishTo,
    () => executables,
    () => falseSecrets,
    () => sdkConstraints,
    () => ignoredAdvisories,
  ]);

  /// Returns the type of dependency from this package onto [name].
  DependencyType dependencyType(String? name) {
    if (dependencies.containsKey(name)) {
      return DependencyType.direct;
    } else if (devDependencies.containsKey(name)) {
      return DependencyType.dev;
    } else {
      return DependencyType.none;
    }
  }
}

/// The type of dependency from one package to another.
enum DependencyType {
  direct,
  dev,
  none;

  @override
  String toString() => name;
}

/// Parses the dependency field named [field], and returns the corresponding
/// map of dependency names to dependencies.
Map<String, PackageRange> _parseDependencies(
  String field,
  YamlNode? node,
  SourceRegistry sources,
  LanguageVersion languageVersion,
  String? packageName,
  ResolvedDescription containingDescription, {
  _FileType fileType = _FileType.pubspec,
}) {
  final dependencies = <String, PackageRange>{};

  // Allow an empty dependencies key.
  if (node == null || node.value == null) return dependencies;

  if (node is! YamlMap) {
    _error('"$field" field must be a map.', node.span);
  }

  final nonStringNode = node.nodes.keys.firstWhereOrNull(
    (e) => e is YamlScalar && e.value is! String,
  );
  if (nonStringNode != null) {
    _error(
      'A dependency name must be a string.',
      (nonStringNode as YamlNode).span,
    );
  }

  node.nodes.forEach((nameNode, specNode) {
    final name = (nameNode as YamlNode).value;
    if (name is! String) {
      _error('A dependency name must be a string.', nameNode.span);
    }
    if (!packageNameRegExp.hasMatch(name)) {
      _error('Not a valid package name.', nameNode.span);
    }
    final spec = specNode.value;
    if (packageName != null && name == packageName) {
      _error('A package may not list itself as a dependency.', nameNode.span);
    }

    final String? sourceName;
    VersionConstraint versionConstraint = VersionRange();
    YamlNode? descriptionNode;
    if (spec == null) {
      sourceName = null;
    } else if (spec is String) {
      sourceName = null;
      versionConstraint = _parseVersionConstraint(
        specNode,
        packageName,
        fileType,
      );
    } else if (specNode is YamlMap) {
      // Don't write to the immutable YAML map.
      final versionNode = specNode.nodes['version'];
      versionConstraint = _parseVersionConstraint(
        versionNode,
        packageName,
        fileType,
      );
      final otherEntries =
          specNode.nodes.entries
              .where((entry) => (entry.key as YamlNode).value != 'version')
              .toList();
      if (otherEntries.length > 1) {
        _error('A dependency may only have one source.', specNode.span);
      } else if (otherEntries.isEmpty) {
        // Default to a hosted dependency if no source is specified.
        sourceName = 'hosted';
      } else {
        switch (otherEntries.single) {
          case MapEntry(key: YamlScalar(value: final String s), value: final d):
            sourceName = s;
            descriptionNode = d;
          case MapEntry(key: final k, value: _):
            _error('A source name must be a string.', (k as YamlNode).span);
        }
      }
    } else {
      _error(
        'A dependency specification must be a string or a mapping.',
        specNode.span,
      );
    }

    // Let the source validate the description.
    final ref = _wrapFormatException(
      'description',
      descriptionNode?.span,
      () {
        return sources(sourceName).parseRef(
          name,
          descriptionNode?.value,
          containingDescription: containingDescription,
          languageVersion: languageVersion,
        );
      },
      packageName,
      fileType,
      targetPackage: name,
    );

    dependencies[name] = ref.withConstraint(versionConstraint);
  });

  return dependencies;
}

/// Parses [node] to a [VersionConstraint].
///
/// `null` is interpreted as [VersionConstraint.any].
///
/// A String is parsed with [VersionConstraint.parse].
VersionConstraint _parseVersionConstraint(
  YamlNode? node,
  String? packageName,
  _FileType fileType,
) {
  if (node?.value == null) {
    return VersionConstraint.any;
  }
  final value = node!.value;
  if (value is! String) {
    _error('A version constraint must be a string.', node.span);
  }

  return _wrapFormatException(
    'version constraint',
    node.span,
    () {
      final constraint = VersionConstraint.parse(value);
      return constraint;
    },
    packageName,
    fileType,
  );
}

/// Runs [fn] and wraps any [FormatException] it throws in a
/// [SourceSpanApplicationException].
///
/// [description] should be a noun phrase that describes whatever's being
/// parsed or processed by [fn]. [span] should be the location of whatever's
/// being processed within the pubspec.
///
/// If [targetPackage] is provided, the value is used to describe the
/// dependency that caused the problem.
T _wrapFormatException<T>(
  String description,
  SourceSpan? span,
  T Function() fn,
  String? packageName,
  _FileType fileType, {
  String? targetPackage,
}) {
  try {
    return fn();
  } on FormatException catch (e) {
    // If we already have a pub exception with a span, re-use that
    if (e is SourceSpanApplicationException) rethrow;

    var msg = 'Invalid $description';
    final typeName = _fileTypeName(fileType);
    if (targetPackage != null) {
      msg =
          '$msg in the "$packageName" $typeName on the "$targetPackage" '
          'dependency';
    }
    msg = '$msg: ${e.message}';
    _error(msg, span);
  }
}

/// Throws a [SourceSpanApplicationException] with the given message.
Never _error(String message, SourceSpan? span, {String? hint}) {
  throw SourceSpanApplicationException(message, span, hint: hint);
}

enum _FileType { pubspec, pubspecOverrides }

String _fileTypeName(_FileType type) {
  switch (type) {
    case _FileType.pubspec:
      return 'pubspec';
    case _FileType.pubspecOverrides:
      return 'pubspec override';
  }
}

/// There are special rules or interpreting SDK constraints, we take care to
/// save the original constraint as found in pubspec.yaml.
class SdkConstraint {
  /// The constraint as written in the pubspec.yaml.
  final VersionConstraint originalConstraint;

  /// The constraint as interpreted by pub.
  final VersionConstraint effectiveConstraint;

  SdkConstraint(
    this.effectiveConstraint, {
    VersionConstraint? originalConstraint,
  }) : originalConstraint = originalConstraint ?? effectiveConstraint;

  /// Implement support for down to 2.12 in the dart 3 series. Note that this
  /// function has to be be idempotent, because we apply it both when we write
  /// and read lock-file constraints, so applying this function a second time
  /// should have no further effect.
  factory SdkConstraint.interpretDartSdkConstraint(
    VersionConstraint originalConstraint, {
    required VersionConstraint? defaultUpperBoundConstraint,
  }) {
    var constraint = originalConstraint;
    if (defaultUpperBoundConstraint != null &&
        constraint is VersionRange &&
        constraint.max == null &&
        defaultUpperBoundConstraint.allowsAny(constraint)) {
      constraint = VersionConstraint.intersection([
        constraint,
        defaultUpperBoundConstraint,
      ]);
    }
    // If a package is null safe it should also be compatible with dart 3.
    // Therefore we rewrite a null-safety enabled constraint with the upper
    // bound <3.0.0 to be have upper bound <4.0.0
    //
    // Only do this rewrite after dart 3.
    if (sdk.version.major >= 3 &&
        constraint is VersionRange &&
        LanguageVersion.fromSdkConstraint(constraint) >=
            LanguageVersion.firstVersionWithNullSafety &&
        // <3.0.0 is parsed into a max of 3.0.0-0, so that is what we look for
        // here.
        constraint.max == Version(3, 0, 0).firstPreRelease &&
        constraint.includeMax == false) {
      constraint = VersionRange(
        min: constraint.min,
        includeMin: constraint.includeMin,
        // We don't have to use .firstPreRelease as the constructor will do that
        // if needed.
        max: Version(4, 0, 0),
      );
    }
    return SdkConstraint(constraint, originalConstraint: originalConstraint);
  }

  /// Flutter constraints get special treatment, as Flutter won't be using
  /// semantic versioning to mark breaking releases. We simply ignore upper
  /// bounds for dependencies.
  ///
  /// After language version
  /// [LanguageVersion.firstVersionRespectingFlutterBoundInRoots] for root
  /// packages we use the upper bound, allowing app developers to constrain the
  /// Flutter version.
  factory SdkConstraint.interpretFlutterSdkConstraint(
    VersionConstraint constraint, {
    required bool isRoot,
    required LanguageVersion languageVersion,
  }) {
    if (constraint is VersionRange) {
      if (!(isRoot && languageVersion.respectsFlutterBoundInRoots)) {
        return SdkConstraint(
          VersionRange(min: constraint.min, includeMin: constraint.includeMin),
          originalConstraint: constraint,
        );
      }
    }
    return SdkConstraint(constraint);
  }

  /// The language version of a constraint is determined from how it is written.
  LanguageVersion get languageVersion =>
      LanguageVersion.fromSdkConstraint(originalConstraint);

  // We currently don't call this anywhere - so this is only for debugging
  // purposes.
  @override
  String toString() {
    if (effectiveConstraint != originalConstraint) {
      return '$originalConstraint (interpreted as $effectiveConstraint)';
    }
    return effectiveConstraint.toString();
  }

  @override
  bool operator ==(Object other) =>
      other is SdkConstraint &&
      other.effectiveConstraint == effectiveConstraint &&
      other.originalConstraint == originalConstraint;

  @override
  int get hashCode => Object.hash(effectiveConstraint, originalConstraint);
}

enum Resolution {
  // Still unused.
  external,
  // This package is a member of a workspace, and should be resolved with a
  // pubspec.yaml located higher.
  workspace,
  // Still unused.
  local,
  // This package is at the root of a workspace.
  none,
}
